/*
 * Tencent is pleased to support the open source community by making TKEStack
 * available.
 *
 * Copyright (C) 2012-2019 Tencent. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use
 * this file except in compliance with the License. You may obtain a copy of the
 * License at
 *
 * https://opensource.org/licenses/Apache-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OF ANY KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations under the License.
 */

// Portions Copyright 2014 The Kubernetes Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package files

import (
	"fmt"
	"os"
	"path/filepath"
	"testing"
	utilfs "tkestack.io/tke/pkg/util/filesystem"
	utiltest "tkestack.io/tke/pkg/util/test"
)

const (
	prefix = "test-util-files"
)

type file struct {
	name string
	// mode distinguishes file type,
	// we only check for regular vs. directory in these tests,
	// specify regular as 0, directory as os.ModeDir
	mode os.FileMode
	data string // ignored if mode == os.ModeDir
}

func (f *file) write(fs utilfs.Filesystem, dir string) error {
	path := filepath.Join(dir, f.name)
	if f.mode.IsDir() {
		if err := fs.MkdirAll(path, defaultPerm); err != nil {
			return err
		}
	} else if f.mode.IsRegular() {
		// create parent directories, if necessary
		parents := filepath.Dir(path)
		if err := fs.MkdirAll(parents, defaultPerm); err != nil {
			return err
		}
		// create the file
		handle, err := fs.Create(path)
		if err != nil {
			return err
		}
		_, err = handle.Write([]byte(f.data))
		if err != nil {
			if cerr := handle.Close(); cerr != nil {
				return fmt.Errorf("error %v closing file after error: %v", cerr, err)
			}
			return err
		}
	} else {
		return fmt.Errorf("mode not implemented for testing %s", f.mode.String())
	}
	return nil
}

func (f *file) expect(fs utilfs.Filesystem, dir string) error {
	path := filepath.Join(dir, f.name)
	if f.mode.IsDir() {
		info, err := fs.Stat(path)
		if err != nil {
			return err
		}
		if !info.IsDir() {
			return fmt.Errorf("expected directory, got mode %s", info.Mode().String())
		}
	} else if f.mode.IsRegular() {
		info, err := fs.Stat(path)
		if err != nil {
			return err
		}
		if !info.Mode().IsRegular() {
			return fmt.Errorf("expected regular file, got mode %s", info.Mode().String())
		}
		data, err := fs.ReadFile(path)
		if err != nil {
			return err
		}
		if f.data != string(data) {
			return fmt.Errorf("expected file data %q, got %q", f.data, string(data))
		}
	} else {
		return fmt.Errorf("mode not implemented for testing %s", f.mode.String())
	}
	return nil
}

// write files, perform some function, then attempt to read files back
// if err is non-empty, expects an error from the function performed in the test
// and skips reading back the expected files
type test struct {
	desc    string
	writes  []file
	expects []file
	fn      func(fs utilfs.Filesystem, dir string, c *test) []error
	err     string
}

func (c *test) write(t *testing.T, fs utilfs.Filesystem, dir string) {
	for _, f := range c.writes {
		if err := f.write(fs, dir); err != nil {
			t.Fatalf("error pre-writing file: %v", err)
		}
	}
}

// you can optionally skip calling t.Errorf by passing a nil t, and process the
// returned errors instead
func (c *test) expect(t *testing.T, fs utilfs.Filesystem, dir string) []error {
	var errs []error
	for _, f := range c.expects {
		if err := f.expect(fs, dir); err != nil {
			msg := fmt.Errorf("expect %#v, got error: %v", f, err)
			errs = append(errs, msg)
			if t != nil {
				t.Errorf("%s", msg)
			}
		}
	}
	return errs
}

// run a test case, with an arbitrary function to execute between write and expect
// if c.fn is nil, errors from c.expect are checked against c.err, instead of errors
// from fn being checked against c.err
func (c *test) run(t *testing.T, fs utilfs.Filesystem) {
	// isolate each test case in a new temporary directory
	dir, err := fs.TempDir("", prefix)
	if err != nil {
		t.Fatalf("error creating temporary directory for test: %v", err)
	}
	c.write(t, fs, dir)
	// if fn exists, check errors from fn, then check expected files
	if c.fn != nil {
		errs := c.fn(fs, dir, c)
		if len(errs) > 0 {
			for _, err := range errs {
				utiltest.ExpectError(t, err, c.err)
			}
			// skip checking expected files if we expected errors
			// (usually means we didn't create file)
			return
		}
		c.expect(t, fs, dir)
		return
	}
	// just check expected files, and compare errors from c.expect to c.err
	// (this lets us test the helper functions above)
	errs := c.expect(nil, fs, dir)
	for _, err := range errs {
		utiltest.ExpectError(t, err, c.err)
	}
}

// simple test of the above helper functions
func TestHelpers(t *testing.T) {
	// omitting the test.fn means test.err is compared to errors from test.expect
	cases := []test{
		{
			desc:    "regular file",
			writes:  []file{{name: "foo", data: "bar"}},
			expects: []file{{name: "foo", data: "bar"}},
		},
		{
			desc:    "directory",
			writes:  []file{{name: "foo", mode: os.ModeDir}},
			expects: []file{{name: "foo", mode: os.ModeDir}},
		},
		{
			desc:    "deep regular file",
			writes:  []file{{name: "foo/bar", data: "baz"}},
			expects: []file{{name: "foo/bar", data: "baz"}},
		},
		{
			desc:    "deep directory",
			writes:  []file{{name: "foo/bar", mode: os.ModeDir}},
			expects: []file{{name: "foo/bar", mode: os.ModeDir}},
		},
		{
			desc:    "missing file",
			expects: []file{{name: "foo", data: "bar"}},
			err:     "no such file or directory",
		},
		{
			desc:    "missing directory",
			expects: []file{{name: "foo/bar", mode: os.ModeDir}},
			err:     "no such file or directory",
		},
	}
	for _, c := range cases {
		t.Run(c.desc, func(t *testing.T) {
			c.run(t, utilfs.DefaultFs{})
		})
	}
}

func TestFileExists(t *testing.T) {
	fn := func(fs utilfs.Filesystem, dir string, c *test) []error {
		ok, err := FileExists(fs, filepath.Join(dir, "foo"))
		if err != nil {
			return []error{err}
		}
		if !ok {
			return []error{fmt.Errorf("does not exist (test)")}
		}
		return nil
	}
	cases := []test{
		{
			fn:     fn,
			desc:   "file exists",
			writes: []file{{name: "foo"}},
		},
		{
			fn:   fn,
			desc: "file does not exist",
			err:  "does not exist (test)",
		},
		{
			fn:     fn,
			desc:   "object has non-file mode",
			writes: []file{{name: "foo", mode: os.ModeDir}},
			err:    "expected regular file",
		},
	}
	for _, c := range cases {
		t.Run(c.desc, func(t *testing.T) {
			c.run(t, utilfs.DefaultFs{})
		})
	}
}

func TestEnsureFile(t *testing.T) {
	fn := func(fs utilfs.Filesystem, dir string, c *test) []error {
		var errs []error
		for _, f := range c.expects {
			if err := EnsureFile(fs, filepath.Join(dir, f.name)); err != nil {
				errs = append(errs, err)
			}
		}
		return errs
	}
	cases := []test{
		{
			fn:      fn,
			desc:    "file exists",
			writes:  []file{{name: "foo"}},
			expects: []file{{name: "foo"}},
		},
		{
			fn:      fn,
			desc:    "file does not exist",
			expects: []file{{name: "bar"}},
		},
		{
			fn:      fn,
			desc:    "neither parent nor file exists",
			expects: []file{{name: "baz/quux"}},
		},
	}
	for _, c := range cases {
		t.Run(c.desc, func(t *testing.T) {
			c.run(t, utilfs.DefaultFs{})
		})
	}
}

// Note: This transitively tests WriteTmpFile
func TestReplaceFile(t *testing.T) {
	fn := func(fs utilfs.Filesystem, dir string, c *test) []error {
		var errs []error
		for _, f := range c.expects {
			if err := ReplaceFile(fs, filepath.Join(dir, f.name), []byte(f.data)); err != nil {
				errs = append(errs, err)
			}
		}
		return errs
	}
	cases := []test{
		{
			fn:      fn,
			desc:    "file exists",
			writes:  []file{{name: "foo"}},
			expects: []file{{name: "foo", data: "bar"}},
		},
		{
			fn:      fn,
			desc:    "file does not exist",
			expects: []file{{name: "foo", data: "bar"}},
		},
		{
			fn: func(fs utilfs.Filesystem, dir string, c *test) []error {
				if err := ReplaceFile(fs, filepath.Join(dir, "foo/bar"), []byte("")); err != nil {
					return []error{err}
				}
				return nil
			},
			desc: "neither parent nor file exists",
			err:  "no such file or directory",
		},
	}
	for _, c := range cases {
		t.Run(c.desc, func(t *testing.T) {
			c.run(t, utilfs.DefaultFs{})
		})
	}
}

func TestDirExists(t *testing.T) {
	fn := func(fs utilfs.Filesystem, dir string, c *test) []error {
		ok, err := DirExists(fs, filepath.Join(dir, "foo"))
		if err != nil {
			return []error{err}
		}
		if !ok {
			return []error{fmt.Errorf("does not exist (test)")}
		}
		return nil
	}
	cases := []test{
		{
			fn:     fn,
			desc:   "dir exists",
			writes: []file{{name: "foo", mode: os.ModeDir}},
		},
		{
			fn:   fn,
			desc: "dir does not exist",
			err:  "does not exist (test)",
		},
		{
			fn:     fn,
			desc:   "object has non-dir mode",
			writes: []file{{name: "foo"}},
			err:    "expected dir",
		},
	}
	for _, c := range cases {
		t.Run(c.desc, func(t *testing.T) {
			c.run(t, utilfs.DefaultFs{})
		})
	}
}

func TestEnsureDir(t *testing.T) {
	fn := func(fs utilfs.Filesystem, dir string, c *test) []error {
		var errs []error
		for _, f := range c.expects {
			if err := EnsureDir(fs, filepath.Join(dir, f.name)); err != nil {
				errs = append(errs, err)
			}
		}
		return errs
	}
	cases := []test{
		{
			fn:      fn,
			desc:    "dir exists",
			writes:  []file{{name: "foo", mode: os.ModeDir}},
			expects: []file{{name: "foo", mode: os.ModeDir}},
		},
		{
			fn:      fn,
			desc:    "dir does not exist",
			expects: []file{{name: "bar", mode: os.ModeDir}},
		},
		{
			fn:      fn,
			desc:    "neither parent nor dir exists",
			expects: []file{{name: "baz/quux", mode: os.ModeDir}},
		},
	}
	for _, c := range cases {
		t.Run(c.desc, func(t *testing.T) {
			c.run(t, utilfs.DefaultFs{})
		})
	}
}

func TestWriteTempDir(t *testing.T) {
	// writing a tmp dir is covered by TestReplaceDir, but we additionally test filename validation here
	c := test{
		desc: "invalid file key",
		err:  "invalid file key",
		fn: func(fs utilfs.Filesystem, dir string, c *test) []error {
			if _, err := WriteTempDir(fs, filepath.Join(dir, "tmpdir"), map[string]string{"foo/bar": ""}); err != nil {
				return []error{err}
			}
			return nil
		},
	}
	c.run(t, utilfs.DefaultFs{})
}

func TestReplaceDir(t *testing.T) {
	fn := func(fs utilfs.Filesystem, dir string, c *test) []error {
		var errs []error

		// compute filesets from expected files and call ReplaceDir for each
		// we don't nest dirs in test cases, order of ReplaceDir call is not guaranteed
		dirs := map[string]map[string]string{}

		// allocate dirs
		for _, f := range c.expects {
			if f.mode.IsDir() {
				path := filepath.Join(dir, f.name)
				if _, ok := dirs[path]; !ok {
					dirs[path] = map[string]string{}
				}
			} else if f.mode.IsRegular() {
				path := filepath.Join(dir, filepath.Dir(f.name))
				if _, ok := dirs[path]; !ok {
					// require an expectation for the parent directory if there is an expectation for the file
					errs = append(errs, fmt.Errorf("no prior parent directory in c.expects for file %s", f.name))
					continue
				}
				dirs[path][filepath.Base(f.name)] = f.data
			}
		}

		// short-circuit test case validation errors
		if len(errs) > 0 {
			return errs
		}

		// call ReplaceDir for each desired dir
		for path, files := range dirs {
			if err := ReplaceDir(fs, path, files); err != nil {
				errs = append(errs, err)
			}
		}
		return errs
	}
	cases := []test{
		{
			fn:      fn,
			desc:    "fn catches invalid test case",
			expects: []file{{name: "foo/bar"}},
			err:     "no prior parent directory",
		},
		{
			fn:      fn,
			desc:    "empty dir",
			expects: []file{{name: "foo", mode: os.ModeDir}},
		},
		{
			fn:   fn,
			desc: "dir with files",
			expects: []file{
				{name: "foo", mode: os.ModeDir},
				{name: "foo/bar", data: "baz"},
				{name: "foo/baz", data: "bar"},
			},
		},
	}
	for _, c := range cases {
		t.Run(c.desc, func(t *testing.T) {
			c.run(t, utilfs.DefaultFs{})
		})
	}
}
